iT邦幫忙

2026 iThome 鐵人賽

DAY 13
1
Claude AI

盡信 Claude,不如無 Code — 心法與全端實戰系列 第 13 篇

Day 13 去識別化做成一支檢查工具:一張對照表,和一條不能交給 AI 的檢查

  • 分享至 

  • xImage
  •  

昨天五個 skill 只有一個被叫過,還差點錯殺兩個。今天要講的東西剛好相反 —— 它只跑過幾次,但第一次跑就抓到一個我人工檢查三個月都沒看到的東西。


問題長什麼樣

寫技術文章最好的素材是真實專案 —— 有 god node、有跑不完的測試、有各種只有真實專案才會長出來的病。

但真實專案裡的名字、路徑、業務用語,沒有一個該出現在公開的文章 repo 裡。

所以每篇文章都要做去識別化:類別名換掉、業務用語換掉、路徑換掉、公司名換掉。這條規則我一直記得,每次發文前人工檢查一遍。

然後我在寫第 28 篇的時候,用工具掃了一次前面 27 篇。

人工檢查漏掉了什麼

第一次全掃的結果:27 篇裡 25 篇有 findings,總共 82 條。

聽起來很糟,但拆開來看是兩半:

分類 數量 說明
我自己規則寫錯造成的誤報 25 兩條壞規則(見下)
規則修對之後剩下的,全是真的 57 分成三種,下面逐一講

第一種,也是唯一一個真的洩密:一個真實的 macOS 使用者名稱,公開了三個月。

它藏在一篇 MCP 教學的設定檔範例裡 —— filesystem server 的 args 和允許目錄清單,中英文版各四處,總共八個位置。「真實路徑必須是零」這條規則我從第一篇就在遵守,而它在那裡躺了 95 天,經過至少三次人工檢查。

這一條是整件事的重點:規則存在、我知道規則、我每次都檢查,而它照樣過了。

第二種:一次手動統一標題,漏了三種東西。

系列的收尾標題早就統一過一次 —— 中文「總結」「參考資料」,英文 Summary / References,一個 commit 動了 12 個檔。那次整理是拿 grep 找「結語」和 Conclusion 去做的,而檢查器掃出來的三種漏法,正好是 grep 的三種盲點:

當時做了什麼 今天仍不合格 漏在哪
結語 → 總結 ✅ 參考資料段還叫「知識來源 / Sources」 修好了搜到的,漏了它的兄弟節
Conclusion: → Summary: ✅ 副標還黏著:Summary: Better Models Won't Save You… 換掉關鍵字,沒滿足規則
完全沒動到 結尾是「Part 6:我的誠實評價」,根本沒有總結段 搜不到一個不存在的東西

三種裡的最後一種最麻煩:那篇是少了一個該有的東西,你沒辦法 grep 一個不存在的字串,所以人工檢查天生看不到它。檢查器不搜關鍵字,它斷言的是結構 —— 最後兩個 H2 必須是「總結」和「參考資料」。這種寫法對「缺少」和「多餘」一樣敏感。

第三種:全形標點,212 處降到 12 處。

這條嚴格說不是洩密,但它的數字有意思:212 → 12 這個落差把「某週開始統一用半形」這件事的時間點精確地標了出來。慣例是哪一天開始的,數據自己會說。

去識別化是一張對照表,不是刪名字

第一次做這件事的人(包括我)會把它理解成「把敏感的字刪掉」。做過幾篇之後才發現它是一張對照表,而且表有兩邊:換什麼、留什麼。兩邊一樣重要。

規則 為什麼
換 業務類別名 → stand-in(固定的替身名),而且跨篇一致 RecordDetailViewController 這種假名字,讀者隔了幾篇再看到要認得出是同一個東西。stand-in 是一個角色,不是一塊馬賽克
換 變數名、元件名也換 最容易漏的一格。類別名換了,morningButton 這種會透露產業的變數名還在 code snippet 裡
換 路徑、公司名、私有套件網址 不解釋
換 圖片裡的文字 截圖要裁掉會顯示真名的面板;表格 PNG 是從 gen.sh 產的,改完 gen.sh 要重新產圖 —— PNG 才是讀者看到的,漏這步等於沒做
留 通用框架名:NSObject、BaseViewController、UIKit 的 delegate 換了反而讓人看不懂,而且它們不具識別性
留 數字 —— 行數、節點數、邊數、測試數 那是度量,不是身分。而且它是文章可信度的來源,換了就沒得驗
說 文章裡放一段聲明:哪些換了、哪些沒換、數字是真的 讀者有權知道他看到的是什麼

這張表有一個結構上的細節:它分成兩半,放在兩個地方。 stand-in 那一半(RecordDetailViewController 這些假名字)寫在 CLAUDE.md 裡,公開,AI 每篇都看得到,所以它每次都用同一套;真名那一半 —— 真的類別名、真的 bundle ID —— 只存在一個不進版控的 pattern 檔裡。AI 只需要看得到前半,就能做對這件事;後半它永遠不需要看到。

這也回答了洩漏是怎麼進來的。我讓 AI 從真實程式碼改寫範例,它照對照表把類別名換了 —— 然後在一個我沒列進表裡的位置,把真名原樣帶了過來:設定檔範例的 args。對照表列到哪,它就換到哪;沒列的,它不會猜。 那八個位置就是這樣躺了 95 天。

這個工具長什麼樣

先講清楚它是什麼,免得聽起來像一套大系統:一支 240 行的 Python,掛成 MCP server,在 Claude Code 裡是一支叫 check_article 的工具。我每次發文前都跑一次,它是我日常發文流程裡唯一一道不靠眼睛的隱私關卡。它對一篇文章(中英兩個 .md 加同資料夾的圖)做 20 條檢查:

類別 檢查 中/英各一
格式 首行 Tags 不超過 5 個;code fence 偶數;結尾兩節是「總結/參考資料」 ✓
圖片 圖片數 = 「插入圖片」提示行數;引用的檔都存在;英文版有沒有漏用 -en 圖 ✓
佔位符 TODO/待補/(文章網址) = 0 ✓
標點 中文版全形標點 = 0 中
去識別化 pattern 檔裡的每一條 = 0 命中 —— 掃正文,也掃產表格圖的 gen.sh ✓ + gen.sh
中英對照 H2 數、H3 數、圖片數兩版相同 —

接法三行,放專案的 .mcp.json:

{ "mcpServers": { "deident-check": {
    "type": "stdio", "command": "uv",
    "args": ["run", "--with", "mcp", "python", "tools/mcp/deident-check/server.py"] } } }

(名字和路徑照你自己的 repo 取,這裡只是形狀;我的 repo 裡它叫 medium-check,原始碼見參考資料。)

uv run --with mcp 讓它不用在全域裝任何東西 —— 這件事 Day 12 講官方 skill 的 PEP 668 坑時提過,同一招。

20 條裡只有三條是去識別化 —— 中文版、英文版,加上產表格圖的 gen.sh。但第一次跑,那個公開了三個月的使用者名稱,就是中英文版那兩條抓到的。 其他 17 條是順手的:既然要掃,那些每次貼文前都要人工看一遍的事一起掃掉,我就不用再看了 —— 前面那兩種(標題、全形)就是順手掃出來的。

這是唯一一條不能交給 AI 的檢查

前面十二天,能寫成程式碼的檢查我都寫成程式碼,其餘交給 AI。這一條不一樣:它必須是程式碼,理由不是準確度,是那份 pattern 檔的內容。

裡面裝的是真實的 bundle ID、team ID、私有套件網址、真實類別名 —— 一份「要藏什麼」的完整清單,本身就是最不該離開這台機器的東西。 把它交給雲端模型判斷「這篇有沒有洩漏」,等於先把要藏的東西整份送出去一次。

所以這一條長這樣,而且三個決定是綁在一起的:

  • 是 regex,不是 prompt —— 判斷不需要理解,只需要比對
  • 在本地跑 —— 檔案不出機器
  • pattern 檔不進版控 —— 進版控的是格式範例 .example.txt,讓別人知道怎麼寫自己的
deident-patterns.txt          # ← 加進 .gitignore
deident-patterns.example.txt  # ← 進版控,只有格式範例

Day 12 講第三方 skill 的安全掃描時,我堅持 --no-llm,內容不上雲 —— 同一條線。這個系列的標題說「盡信 Claude,不如無 Code」,大部分時候講的是 AI 交出來的東西要驗;走到隱私這一端,它的形狀變成:有些裁判必須是你自己,因為告訴 AI 要找什麼,就是洩漏本身。

誠實標一個邊界:regex 擋得住「字面上的洩漏」,擋不住「語意上的」—— 一段描述具體到讓人猜得出是哪家公司,沒有 pattern 抓得到。那一層只能靠寫的時候的判斷,不是檢查的時候。


三個第一版踩到的坑

第一,pattern 要能認得「已經處理過」的樣子。

原本的路徑 pattern 長這樣:

/Users/[a-z]+/

它會抓到真實使用者名稱 —— 但它也會抓到我剛剛才改好的 /Users/you/。修復動作本身觸發了警報,而且每次重跑都會再觸發一次。

改成排除已知的佔位符:

/Users/(?!you/|username/|USER/)[a-z0-9]+/

負向前瞻讓它學會「這個已經是安全的了」。

這件事有一個更一般的形式:任何自動檢查,都必須能區分「還沒修」和「已經修好」。分不出來的檢查,你會在第三次重跑之後開始忽略它 —— 而那就是它死掉的時候。

第二,連圖都要掃 —— 但不是掃圖,是掃產圖的腳本。

表格 PNG 是讀者唯一看得到的東西。PNG 裡的文字沒辦法 grep,但產生它的 gen.sh 可以。所以檢查要涵蓋 gen.sh,而且改完 gen.sh 一定要重新產圖 —— 漏了這一步,等於沒去識別化。

第三,誠實面對自己的壞規則。

82 條 findings 裡有 25 條是我自己規則寫錯:一條把文章正文裡「TODO」這個字當成待補標記(而那幾篇文章正好在講怎麼用 hook 擋 TODO),另一條要求每張截圖都要有 -en 版本(但沒有文字的截圖中英共用是正確的)。

誤報要當 bug 修,不是當雜訊忽略。 一個誤報率高的檢查,三週後你就會養成「掃過去不看」的習慣,那時候它跟不存在沒有差別。

這三天做的是同一件事

這三天(11、12、13)做的其實是同一件事:把自己每天要重複做的事寫成檔案 —— 一句常講的話變成指令、一套流程變成 skill、一份發文前的檢查變成程式。然後回頭確認兩件事:它們還活著嗎?它們還抓得到東西嗎?

這一篇留下的心法:

去識別化要做成系統:一張跨篇一致的對照表,加一條每次發文前都跑的檢查 —— 這條不能交給 AI。誤報要當 bug 修,不是當雜訊忽略;一個常誤報的檢查,三週後你會養成掃過去不看的習慣。

明天:換一個方向 —— 不是自己寫,是交出去。MCP 到底是什麼?不看教學文,直接用手打一次 JSON-RPC —— 規格、SDK、你手上這支,三個答案要打一次才知道。


參考資料


上一篇
Day 12 寫完的 skill 要回來數,裝來的 skill 要先隔離
下一篇
Day 14 MCP 支援 X?規格、SDK、你手上這支,三個答案要打一次才知道
系列文
盡信 Claude,不如無 Code — 心法與全端實戰 共 18 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

1
helenanova
iT邦新手 5 級 ‧ 2026-09-27 19:12:02

「規則存在、我知道規則、我每次都檢查,而它照樣過了」是全文最值錢的一句。依賴人記得執行的檢查不是檢查,是祈禱——它賭的是那個人那天記得做、而且狀態夠好。下一步我會把這支工具接進發文流程本身:pre-publish hook 或 CI gate,findings 不歸零就發不出去。工具存在和工具擋得住是兩回事,只有放在唯一的發佈路徑上才算數。另外那 25 條誤報其實是關鍵指標:會亂叫的檢查遲早被人繞過,規則的精確度決定這道 gate 能不能長期活著。

看更多先前的回應...收起先前的回應...
mikewang iT邦新手 5 級 ‧ 2026-09-27 20:26:02 檢舉

謝謝,你把我想講的話說得更準了。

補一個我自己也卡住的地方:發佈的最後一步,是把稿子貼進 iThome、Medium 的網頁編輯器,這一段沒有我能控制的 hook,所以沒有「唯一的發佈路徑」可以擋。

我能做的是把 gate 往前移,移到檔案被寫進去的那一刻。這個 repo 現在有一支 PreToolUse hook,Claude 要寫進文章檔之前,先跑去識別化檢查,命中就不給寫。

「最後一哩沒有 hook 可以擋」是真的,這種情況我會反過來做 read-back:發佈後把線上版本抓回來,對「實際刊出的內容」再跑一次同一支檢查。gate 擋不了的地方,就用刊出後驗證補——寫進去的是什麼不重要,讀回來的才算數。搭配你現在的 PreToolUse hook,等於寫入前一道、刊出後一道,中間貼錯版本或手滑改到都還抓得到。

mikewang iT邦新手 5 級 ‧ 2026-09-29 10:06:14 檢舉

謝謝,這一步我其實有在做,只是沒寫進文章:每篇發完,我都會把線上版抓回來,跟本機的發文版逐段比對,標題、小節、表格、程式碼區塊、連結,一樣一樣核對。
不過你上次說中的問題也還在:這個檢查目前還是要靠自己記得執行。

「還要靠自己記得執行」其實是這整套裡最脆弱的一道——靠記憶的 gate 遲早會漏,失效率跟沒有是一樣的,只是時間問題。建議把逐段比對做成一支 script:抓線上版、正規化回 markdown、跟本機發文版 diff,一個指令跑完。觸發也不用靠記得:既然文章檔已經在你的 repo 裡,寫完檔的那個 session 結束時可以用 Stop hook 丟出「還沒跑 publish-check」的提示,或讓發佈後回來打的標記當觸發條件。原則是:值得做的檢查值得一個觸發條件,而不是一個提醒。

mikewang iT邦新手 5 級 ‧ 2026-10-01 14:50:10 檢舉

謝謝,你說的第一步這兩天剛好做了:抓線上版、剝掉格式、跟本機發文版逐段比對,現在是一支 script,一個指令跑完。這週回頭改了幾篇已發的文章,就是靠它確認改到的是對的地方 —— 其中一次還真的抓到我改錯段落。

觸發條件你說的第二種比較合我的流程。寫稿的 session 結束時文章還沒發,發文是隔天在網頁上手貼,所以 Stop hook 的時機對不上;真正代表「發完了」的,是我回來在發文紀錄裡填上網址那一刻。

把「填上網址」當觸發,比 Stop hook 準,因為它對應的是真的發生了的事。可以再往前一步:讓比對 script 自己讀發文紀錄,看到新網址就跑,結果(通過,或差在哪一段)寫回同一列。沒有結果的列就算沒做完,「忘了跑」會變成紀錄上看得到的空格,不用靠記得。

另外你抓到過改錯段落那次,建議把它存成固定的測試輸入。之後改正規化規則時,先確認這個案例還抓得到,才知道比對本身沒有悄悄退化。

我要留言

立即登入留言